<?php
/**
 * Library Of Shared Code (LOSC)
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @category   LOSC Framework
 * @package    Controller
 * @subpackage Crud
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */

/**
 * Represents a CRUD result
 *
 * @category   LOSC Framework
 * @package    Controller
 * @subpackage Crud
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */
class Losc_Controller_Crud_Result
{
    /**#@+
     * CRUD result code constant.
     */
    
    const ERROR_PARSE           = -32700;
    const ERROR_INVALID_REQUEST = -32600;
    const ERROR_INVALID_METHOD  = -32601;
    const ERROR_INVALID_PARAMS  = -32602;
    const ERROR_INTERNAL        = -32603;
    const ERROR_APPLICATION     = -32500;
    const ERROR_SYSTEM          = -32400;
    const ERROR_TRANSPORT       = -32300;
    const ERROR_OTHER           = -32000;
    const ERROR_NOT_FOUND       = -32004;
    const ERROR_BAD_HTTP_METHOD = -32005;
    
    const SUCCESS_FOUND   = 52400;
    const SUCCESS_CREATED = 52300;
    const SUCCESS_UPDATED = 52200;
    const SUCCESS_DELETED = 52100;
    const SUCCESS_OTHER   = 52000;
    
    /**#@- ------------------------------------------------------------------ */
    
    /**#@+
     * Static.
     */
    
    /**
     * Maps response codes to HTTP status codes
     *
     * @var array
     */
    private static $_HTTP_CODES = array(
        self::ERROR_PARSE           => 400,
        self::ERROR_INVALID_REQUEST => 400,
        self::ERROR_INVALID_METHOD  => 400,
        self::ERROR_INVALID_PARAMS  => 400,
        self::ERROR_INTERNAL        => 500,
        self::ERROR_APPLICATION     => 500,
        self::ERROR_SYSTEM          => 500,
        self::ERROR_TRANSPORT       => 500,
        self::ERROR_OTHER           => 500,
        self::ERROR_NOT_FOUND       => 404,
        self::ERROR_BAD_HTTP_METHOD => 405,
        
        self::SUCCESS_FOUND   => 200,
        self::SUCCESS_CREATED => 200, // TODO: use 201?
        self::SUCCESS_UPDATED => 200,
        self::SUCCESS_DELETED => 200,
        self::SUCCESS_OTHER   => 200
    );
    
    /**
     * Maps response codes to default result messages
     *
     * @var array
     */
    private static $_DEFAULT_MESSAGES = array(
        self::ERROR_PARSE           => 'Parse error (not well-formed).',
        self::ERROR_INVALID_REQUEST => 'Invalid request.',
        self::ERROR_INVALID_METHOD  => 'Method not found.',
        self::ERROR_INVALID_PARAMS  => 'Invalid parameters.',
        self::ERROR_INTERNAL        => 'Internal error.',
        self::ERROR_APPLICATION     => 'Application error.',
        self::ERROR_SYSTEM          => 'System error.',
        self::ERROR_TRANSPORT       => 'Transport error.',
        self::ERROR_OTHER           => 'Unknown error.',
        self::ERROR_NOT_FOUND       => 'No objects found.',
        self::ERROR_BAD_HTTP_METHOD => 'Invalid request method.',
        
        self::SUCCESS_FOUND   => 'Found object(s).',
        self::SUCCESS_CREATED => 'Created object.',
        self::SUCCESS_UPDATED => 'Updated object.',
        self::SUCCESS_DELETED => 'Deleted object.',
        self::SUCCESS_OTHER   => 'Operation successful.'
    );
    
    /**
     * Used for translating messages
     *
     * @var Zend_Translate_Adapter
     */
    private static $_translator;
    
    /**#@- ------------------------------------------------------------------ */
    
    /**
     * Result code
     *
     * @var int
     */
    private $_code;
    
    /**
     * Result message
     *
     * @var string
     */
    private $_message;
    
    /**
     * Result data (extra messages or records)
     *
     * @var array
     */
    private $_data;
    
    /**
     * Fields that shouldn't be exposed
     *
     * @var array
     */
    private $_hiddenFields = array();
    
    /**
     * Relations that shouldn't be exposed
     *
     * @var array
     */
    private $_hiddenRelations = array();
    
    /**
     * Creates a CRUD result
     *
     * @param int          $code     one of the result class constants
     * @param string|array $message  [optional] result message, optionally an
     *                               array, in which case the first element will
     *                               be used as the message, and the rest of the
     *                               elements as value substitutions. If not
     *                               given, it will be determined from the 
     *                               $code parameter.
     * @param array        $data     [optional] result data. May only contain
     *                               Doctrine_Record instances if the result is
     *                               successful, and only strings (extra 
     *                               messages) if the result is en error result.
     *                               Elements not matching will be filtered out.
     */
    public function __construct($code, $message = null, array $data = null)
    {
        if (!array_key_exists($code, self::$_HTTP_CODES)) {
            // code is not valid
            $code = ((int) $code > 0) ? self::SUCCESS_OTHER : self::ERROR_OTHER;
        }
        
        // set code
        $this->_code = $code;
        
        $this->setMessage($message);
        $this->setData($data);
    }
    
    /**
     * Sets result message
     *
     * @param  string|array $message  [optional] if not given, it will be 
     *                                determined using the result code. If an
     *                                array is given, the first element is used
     *                                as message, and the rest of the elements
     *                                as value substitutions.
     * @return Losc_Controller_Crud_Result
     */
    public function setMessage($message = null)
    {
        if (is_array($message)) {
            $msg = array_shift($message);
            
            if ($t = self::getTranslator()) {
                $msg = $t->translate($msg);
            }
            
            if (count($message)) {
                $message = vsprintf($msg, $message);
            }
        } elseif (!is_string($message) || empty($message)) {
            // message not given, use default
            if ($t = self::getTranslator()) {
                $message = $t->translate(self::$_DEFAULT_MESSAGES[$this->_code]);
            }
        } else {
            // message given, translate it
            if ($t = self::getTranslator()) {
                $message = $t->translate($message);
            }
        }
        
        $this->_message = $message;
        
        return $this;
    }
    
    /**
     * Returns result message
     *
     * @return string
     */
    public function getMessage()
    {
        return $this->_message;
    }
    
    /**
     * Sets result data
     * 
     * The $data param will be filtered like this;
     * - if this is an error result, the $data may only contain strings
     * - if the result is successful, the $data may only contain records
     * - if not record or string, the element will be filtered out
     *
     * @param  array $data  [optional] if not given, data will be reset
     * @return Losc_Controller_Crud_Result
     */
    public function setData(array $data = null)
    {
        $this->_data = array();

        if ($data !== null) {
            if ($this->isSuccess()) {
                // if success, data should only contain records
                foreach ($data as $record) {
                    if ($record instanceof Doctrine_Record) {
                        $this->_data[] = $record;
                    }
                }
            } else {
                // if error, data should only contain strings
                foreach ($data as $extraMessage) {
                    if (is_string($extraMessage)) {
                        $this->_data[] = $extraMessage;
                    }
                }
            }
        }
        
        return $this;
    }
    
    /**
     * Returns result data
     * 
     * If the result is successful, the returned array will only contain
     * records. If the result is an error, the returned array will only contain
     * strings (extra error messages).
     * 
     * If $raw is true, the records will be returned as-is. Default is to return
     * a normalized array.
     *
     * @param  boolean $prefixKey  [optional] if true, relations and stuff will
     *                             be prefixed in the returned array
     * @param  boolean $raw        [optional] if true, raw records are returned
     * @return array
     */
    public function getData($prefixKey = null, $raw = null)
    {
        if ($raw || $this->isError()) {
            return $this->_data;
        }
        
        $data = array();
        
        foreach ($this->_data as $key => $record) {
            $recordData = $record->toArray(false, $prefixKey);
            foreach ($this->_hiddenFields as $hiddenField) {
                unset($recordData[$hiddenField]);
            }
            
            $refs = $record->getReferences();
            foreach ($refs as $alias => $relation) {
                if (!in_array($alias, $this->_hiddenRelations)) {
                    $recordData[$alias] = $relation->toArray(false, $prefixKey);
                }
            }
            
            if ($prefixKey) {
                $data[get_class($record) . "_$key"] = $recordData;
            } else {
                $data[$key] = $recordData;
            }
        }
        
        return $data;
    }
    
    /**
     * Returns result code
     *
     * @return int
     */
    public function getCode()
    {
        return $this->_code;
    }
    
    /**
     * Returns the HTTP status code associated with the result's code
     *
     * @return int
     */
    public function getHttpCode()
    {
        return self::$_HTTP_CODES[$this->_code];
    }
    
    /**
     * Checks if the result is an error result
     *
     * @return boolean
     */
    public function isError()
    {
        return $this->_code < 1;
    }
    
    /**
     * Checks if the result is a successful result
     *
     * @return boolean
     */
    public function isSuccess()
    {
        return $this->_code > 0;
    }
    
    /**
     * Hides field(s)
     *
     * @param  string|array $field  field name or array of field names
     * @return Losc_Controller_Action_DoctrineCrud
     */
    public function hideField($field)
    {
        if (is_array($field)) {
            $this->_hiddenFields = array_merge($this->_hiddenFields, $field);
        } else {
            $this->_hiddenFields[] = $field;
        }
        
        return $this;
    }
    
    /**
     * Hides relation(s)
     *
     * @param  string|array $relation  relation name or array of relation names
     * @return Losc_Controller_Action_DoctrineCrud
     */
    public function hideRelation($relation)
    {
        if (is_array($relation)) {
            $this->_hiddenRelations = array_merge($this->_hiddenRelations,
                                                  $relation);
        } else {
            $this->_hiddenRelations[] = $relation;
        }
        
        return $this;
    }
    
    /**
     * Returns an array representation of the result
     *
     * @param  boolean $prefixKey  [optional] whether the keys in records
     *                             should be prefixed with component name
     * @return array
     */
    public function toArray($prefixKey = null)
    {
        return array(
            'code'     => $this->getCode(),
            'httpCode' => $this->getHttpCode(),
            'message'  => $this->getMessage(),
            'data'     => $this->getData($prefixKey)
        );
    }
    
    /**
     * Sets a translator
     *
     * @param  Zend_Translate|Zend_Translate_Adapter|null $translator
     * @throws Losc_Exception  if an invalid translator is given
     */
    public static function setTranslator($translator)
    {
        if ($translator instanceof Zend_Translate) {
            self::$_translator = $translator->getAdapter();
        } elseif ($translator instanceof Zend_Translate_Adapter) {
            self::$_translator = $translator;
        } elseif (is_null($translator)) {
            self::$_translator = null;
        } else {
            $msg = '$translator must an instance of Zend_Translate or '
                 . 'Zend_Translate_Adapter, or null.';
            throw new Losc_Exception($msg);
        }
    }
    
    /**
     * Returns translator if found, or null if not found
     *
     * @return Zend_Translate_Adapter|null
     */
    public static function getTranslator()
    {
        if (!self::$_translator) {
            self::$_translator = self::_getDefaultTranslator();
        }
        
        return self::$_translator;
    }
    
    /**
     * Returns translator from {@link Zend_Registry} if found
     *
     * @return Zend_Translate_Adapter|null
     */
    private static function _getDefaultTranslator()
    {
        if (Zend_Registry::isRegistered('Zend_Translate')) {
            $t = Zend_Registry::get('Zend_Translate');
            if ($t instanceof Zend_Translate) {
                return $t->getAdapter();
            } elseif ($t instanceof Zend_Translate_Adapter) {
                return $t;
            }
        }
        
        return null;
    }
}
